#include <stdio.h>

//在程序设计过程中,为了实现某个功能需要编写多行代码,例如求一个二维数组中的最大值,如果该功能需要被多次使用,我们可以在每次使用时将原来的代码重复编写,
// 但是这样未免有"凑代码"的嫌疑,而且编程效率也不高。为了避免重复劳动,我们可以将这部分代码封装到一个"函数"中,在需要使用该功能时调用封装好的函数即可。
/**
 * 函数定义的语法规则:
 * 类型标识符  函数名(形参列表)
 * {
 *      函数体
 * }
 * 1)类型标识符：函数返回数据的类型
 * 2)函数名：有用户自定义的标识符
 * 3)形参列表：函数用来接收用户所传递数据的参数——调用该方法需要传递的参数，定义时参数可以是0个也可以是多个，根据实际需求而定
 * 4)函数体：实现函数功能的代码块——要执行的代码
 * TIPS:C程序是从上往下执行的，所以函数的定义必须放置在main()函数的上面——先定义才有被使用的可能。定义后的函数不被主动调用的情况下是不会被执行的。
 *
 * 求数组最大值的函数
 * @param arr 需要操作的数组
 * @param len 数组长度
 * @return 数组最大值
 */
int array_max(int arr[], int len) {
    int j, max = arr[0];
    for (j = 0; j < len; ++j) {
        if (arr[j] > max) max = arr[j];
    }
    return max;
}

/**
 * 1、如果函数不需要接收用户传递的数据，则可以将形参列表置为空
 * 2、如果函数不需要返回值，则"类型标识符"可以设计为 void，表示该函数不返回任何值
 * 3、函数的返回类型就是函数的类型
 */
void func() {
    printf("比如说我希望这个函数只做输出这句话的作用，就可以这么定义\n");
}

/**
 * 函数的形参和实参。
 * 形参：出现在函数的定义中，在整个函数体中都可以访问到，离开了函数就不能被访问到了
 * 实参：函数调用时实际传递的参数。
 * 函数的调用本质是将"实参的值"赋值给"形参"——这意味着对形参的值进行修改并不会影响到实参
 * 形参特点：只有在函数被调用时才会被分配内存空间
 * 实参特点：可以是常量、变量、表达式、函数名
 */
int max(int a, int b) {
    printf("形参a的值:%d\n", a);
    printf("形参b的值:%d\n", b);
    printf("形参a的地址:%p\n", &a);
    printf("形参b的地址:%p\n", &b);
    //修改a和b的值
    a++;
    b++;
    //函数的返回值——返回值是函数将函数体中的"执行结果"返回(传递)给函数的调用者，而实现这一点的就是"return关键字"
    //语法: return 表达式
    //TIPS:当函数执行完return语句后，表示函数执行完毕，则函数将直接退出，不再执行
    return a > b ? a : b;
}

/**
 * 当函数有返回值时，我们在调用后可以定义一个变量来接收，此时变量跟函数的返回值类型必须一致。当然我们也可以不接收直接使用返回值。
 */
double test(int command) {
    if (command) {
        printf("即将因为return的作用而直接退出函数\n");
        //测试return的函数直接退出功能
        //当函数返回类型跟return语句中的值数据类型不一样时，则以函数类型为准，将自动进行类型转换
        return 1;//因为函数返回类型是double,所以这个int类型的1会转成double类型再返回
    }
    printf("由于return已经将函数退出，所以这段话是不会输出的\n");
    return 0;
}

/**
 * 函数声明。语法: 函数类型  函数名称(形参列表)
 * 1、后面有封号 ;
 * 2、函数声明时，函数类型(返回值类型)必须与函数实现时一致，函数名必须和函数实现时一致
 * 3、形参列表中，可以不填写形参变量的名字，只需要填写形参的类型即可。
 */
void func2(int);

/**
 * 函数的嵌套使用：某个函数的函数体调用了另外一个函数——比较显而易见一个概念。
 * 比如说我们使用main函数时其实就已经嵌套使用过了printf函数以及本例中的很多函数了。
 * 当然，如果想继续多层嵌套下去也是完全可以的。当然C程序最后依旧会在main函数执行完毕后结束。
 *
 * 这里主要是演示函数声明需要注意的地方。
 */
void func1() {
    printf("这里是函数1\n");
    //异常:Implicit declaration of function 'func2' is invalid in C99。翻译是:C99中隐式声明函数是无效的。
    // 虽然如此，但是使用着MinGW64的我们依旧可以成功编译甚至成功执行。但是如果编译器的版本较低，则会编译报错:函数未声明。
    //众所周知，C程序是从上往下解析的，而目前func2在func1的下方，这种情况下编译器在编译代码时，会在该语句之前检查函数是否有被声明或定义，没有则报错。

    //解决办法：1、调换func1和func2的位置  2、将func2函数的"定义"放在func1之前。
    func2(3);
}

void func2(int a) {
    printf("这里是函数2\n");
    //在下方的函数可以调用到上方的函数且不会出现异常
//    func1();
}

/**
 * main函数是C程序的主函数，会被自动执行，C程序中有且只能有一个main函数
 */
int main() {
    //函数就是具有特殊功能的指令的集合
    //C程序是由函数组成的，函数是C程序的基本模块，比如说C程序就是从主函数main()开始执行的。
    //从函数定义的角度出发，函数可以分为"库函数"和"用户自定义函数"两种。
    //·库函数：有标准C库提供，用户不需要自己实现，在使用时直接调用即可。
    printf("比如说我们最常用的printf()就是一个库函数，这种我们并不需要自己实现的就是\"库函数\"\n");
    //·用户自定义函数：用户自己根据实际的需要自己实现的函数。比如说我想有个求数组最大值的函数
    int arr[4] = {10, 5, 90, 7};
    printf("该数组的最大值为(通过自己实现的函数):%d\n", array_max(arr, 4));
    func();

    int a = 2, b = 3;
    //调用该函数传递的这两个参数就是实参
    //TIPS:实参和形参是一一对应的，位置和个数之类的不要搞错
    int maxValue = max(a, b);
    //可以看到在函数内对形参的修改并未影响到实参
    printf("调用函数后实参a的值:%d\n", a);
    printf("调用函数后实参b的值:%d\n", b);
    //不影响到实参的原因(从内存角度上看):
    printf("实参a的地址:%p\n", &a);
    printf("实参b的地址:%p\n", &b);
    printf("调用函数后返回值是:%d\n", maxValue);
    printf("调用函数后返回值是:%f\n", test(2));
    func1();

    //声明一个递归函数(TIPS:在函数内部没法直接定义函数体，但是是可以进行声明的)
    long long factorial(int);
    printf("测试递归函数:%lld\n", factorial(4));
    return 0;
}

/**
 * 递归函数——在实现某个函数的过程中，某个功能的实现与本函数一样，则会出现在函数内部的实现中再次调用自己的情况，这种就叫做"函数的递归"。
 * TIPS:函数递归调用必须有退出条件!!
 *
 * 定义一个求n的阶乘的函数。例子：3的阶乘=3×2×1
 * @param n 阶乘
 * @return
 */
long long factorial(int n) {
    if (n == 1) return 1;
    return n * factorial(n - 1);
}
